package roboGP;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Random;

/**
 * Rappresenta un robot nella sua interezza.
 */
public class MetaRobot 
{
	private final String ROBOT = "Robot_";
	public static String robotTemplate = null;
	
	private RobotGenoma _genoma;
	private int _nGen;
	private int _nRobot;
	private String _name;
	private double _fitness;
	
	public MetaRobot(int nGen, int nRobot, RobotGenoma genoma)
	{
		_nGen = nGen;
		_nRobot = nRobot;
		_name = ROBOT+_nGen+"_"+_nRobot;
		_genoma = genoma;
		_fitness = 0.00001; //Per evitare divisioni con 0
	}
	
	public MetaRobot()
	{
		_nGen = -1;
		_nRobot = -1;
		_name = "";
		_genoma = null;
		_fitness = -1;
	}
	
	/**
	 * Crea il codice del Robot, ricavandolo dai rispettivi Tree, e lo salva in un file .java.
	 */
	public void createRobot()
	{
		if (robotTemplate == null)
		{
			try {
				robotTemplate = new String(Files.readAllBytes(Paths.get("txt/RobotJavaTemplate.txt")));
			} 
			catch (IOException e) 
			{
				e.printStackTrace();
			}
		}
		
		String sourceCode = new String(robotTemplate);
		
		sourceCode = sourceCode.replace("$package$", EvolutionChamber.PACKAGE);
		sourceCode = sourceCode.replace("$className$", _name);
		sourceCode = sourceCode.replace("$targetMain$", _genoma.getMain().toJavaCode(3));
		sourceCode = sourceCode.replace("$targetOnScannedRobot$", _genoma.getOnScannedRobot().toJavaCode(2));
		sourceCode = sourceCode.replace("$targetOnHitByBullet$", _genoma.getOnHitByBullet().toJavaCode(2));
		sourceCode = sourceCode.replace("$targetOnHitWall$", _genoma.getOnHitWall().toJavaCode(2));
		sourceCode = sourceCode.replace("$targetOnHitRobot$", _genoma.getOnHitRobot().toJavaCode(2));

		try {
			FileWriter fstream = new FileWriter(EvolutionChamber.PATH +"/"+_name+".java");
			BufferedWriter out = new BufferedWriter(fstream);
			out.write(sourceCode);
			out.close();
		}
		catch(Exception e) {
			System.err.println("Error: " + e.getMessage());
		}
		
		compileRobot();
	}
	
	/**
	 * Compila il sorgente del Robot.
	 * @return Il codice del Robot compilato.
	 */
	public String compileRobot() 
	{
		try {
			execute("javac -cp " + EvolutionChamber.JARS + " " +EvolutionChamber.PATH + "/ACRobot.java"+ " " + EvolutionChamber.PATH + "/" + _name + ".java");
		}catch(Exception e){
			e.printStackTrace();
		}
		return (_name+".class");
	}
	
	/**
	 * Esegue un comando sulla riga di comando.
	 * @param command Il comando da eseguire.
	 * @throws Exception Problemi, problemi, problemi.
	 */
	public static void execute(String command) throws Exception {
		Process process = Runtime.getRuntime().exec(command);
		printMsg(command + " stdout:", process.getInputStream());
		printMsg(command + " stderr:", process.getErrorStream());
		process.waitFor();
		if(process.exitValue() != 0)
			System.out.println(command + "exited with value " + process.exitValue());
	}
	
	private static void printMsg(String name, InputStream ins) throws Exception {
		String line = null;
		BufferedReader in = new BufferedReader(new InputStreamReader(ins));
		while((line = in.readLine()) != null){
			System.out.println(name + " " + line);
		}
	}
	
	/**
	 * Cancella i file .java e .class non più utilizzati, della generazione precedente, tranne quelli del robot migliore.
	 * @param gen Numero della generazione da cancellare.
	 * @param pop_size Numero di elementi della generazione.
	 */
	public static void clearBots(int best, int gen, int pop_size) {	
		
		File oldJava, oldClass;
		
		for(int i = 0; i < pop_size; i++){
			if (i==best) continue;
			oldJava = new File(EvolutionChamber.PATH+"/"+"Robot_"+gen+"_"+i+".java");
			oldClass = new File(EvolutionChamber.PATH+"/"+"Robot_"+gen+"_"+i+".class");
			oldJava.delete();
			oldClass.delete();
		}
	}
	/**
	 * Esegue il crossover effettuando una selezione di tipo "tournament select".
	 * 
	 * @param metaRobots La lista dei robot della generazione precedente.
	 * @param nRobots Il numero di robot scelti in modo random per il tournament select.
	 * @param nGen La generazione di cui il robot fa parte.
	 * @param nMetaBot Il numero del robot all'interno della generazione.
	 * @return afterCrossover Due nuovi genomi, generati dal crossover.
	 */
	public static MetaRobot[] robotCrossover(MetaRobot[] metaRobots, int nRobots, int nGen, int nMetaBot) {
		MetaRobot[] afterCrossover = new MetaRobot [2];
		MetaRobot parent1 = tournamentSelect(metaRobots, nRobots);
		MetaRobot parent2 = tournamentSelect(metaRobots, nRobots);
		RobotGenoma genoma1 = parent1.getGenoma().copyGenoma();
		RobotGenoma genoma2 = parent2.getGenoma().copyGenoma();
		
		RobotGenoma.crossoverGenoma(genoma1, genoma2);
		
		afterCrossover[0] = new MetaRobot (nGen, nMetaBot, genoma1);
		afterCrossover[1] = new MetaRobot (nGen, nMetaBot+1, genoma2);
		
		return afterCrossover;
	}
	
	/**
	 * Esegue il crossover tra due robot, restituendo solo uno dei due prodotti.
	 * 
	 * @param metaRobots La lista dei robot della generazione precedente.
	 * @param nRobots Il numero di robot scelti in modo random per il tournament select.
	 * @param nGen La generazione di cui il robot fa parte.
	 * @param nMetaBot Il numero del robot all'interno della generazione.
	 * @return Un nuovo robot.
	 */
	public static MetaRobot singleRobotCrossover(MetaRobot[] metaRobots, int nRobots, int nGen, int nMetaBot)
	{
		MetaRobot parent1 = tournamentSelect(metaRobots, nRobots);
		MetaRobot parent2 = tournamentSelect(metaRobots, nRobots);
		RobotGenoma genoma1 = parent1.getGenoma().copyGenoma();
		
		RobotGenoma.crossoverGenoma(genoma1, parent2.getGenoma().copyGenoma());
		
		return new MetaRobot (nGen, nMetaBot, genoma1);
	}
	
	public static MetaRobot tournamentSelect(MetaRobot[] metaRobots, int nRobots) {
		MetaRobot[] robotsToSelect = new MetaRobot[nRobots];
		Random randValue = new Random();
		for (int i=0; i<nRobots;i++) {
			int robot = randValue.nextInt(metaRobots.length);
			robotsToSelect[i] = metaRobots[robot];
		}
		return findBest(robotsToSelect);
	}
	
	/**
	 * Trova il migliore da una selezione di robot.
	 * @param metaRobots Una selezione (casuale) di MetaRobots.
	 * @return Il miglior MetaRobot della selezione passata come parametro.
	 */
	public static MetaRobot findBest(MetaRobot[] metaRobots) {
		double max = -1;
		int robotMaxFitness = -1;
	    for(int j=0; j<metaRobots.length; j++) {
	        if(max < metaRobots[j]._fitness){
	            max = metaRobots[j]._fitness;
	        	robotMaxFitness = j;
	        }
	    }
		return metaRobots[robotMaxFitness];
	}
	
	/**
	 * Trova i migliori n robot in una collezione.
	 * @param metaRobots Una collezione di Robot.
	 * @param n Il numero di robot da trovare.
	 * @return I robot migliori.
	 */
	public static ArrayList<MetaRobot> findBest(MetaRobot[] metaRobots, int n) {
		ArrayList<MetaRobot> best = new ArrayList<MetaRobot>(n);
		
		int found = 0;
		while (found < n)
		{
			double max = -1;
			int robotMaxFitness = -1;
		    for(int j=0; j<metaRobots.length; j++) 
		    {
		        if(max < metaRobots[j].getFitness() && !best.contains(metaRobots[j]))
		        {
		            max = metaRobots[j].getFitness();
		        	robotMaxFitness = j;
		        }
		    }
		    found++;
		    best.add(metaRobots[robotMaxFitness]);
		}
		
		return best;
	}
	
	public String getName()
	{
		return _name;
	}
	
	public double getFitness()
	{
		return _fitness;
	}
	
	public void setFitness(double fitness)
	{
		_fitness = fitness;
	}
	
	public RobotGenoma getGenoma()
	{
		return _genoma;
	}
	
	public int getNRobot(){
		return _nRobot;
	}
	
	public static MetaRobot mutateRobot(MetaRobot[] metaRobots, boolean subtree, int nRobots, int nGen, int nMetaBot)
	{
		MetaRobot mr = tournamentSelect(metaRobots, nRobots);
		RobotGenoma mutedRG = mr.getGenoma().mutateGenoma(subtree);
		return new MetaRobot(nGen, nMetaBot, mutedRG);
	}
	
	/**
	 * Compila il sorgente di ACRobot.
	 * 
	 * @param sourceCode Una stringa con il codice sorgente del Robot.
	 * @return Il codice del Robot compilato.
	 */
	public static String compileACRobot() {
		String name = "ACRobot";
		try {
			execute("javac -cp " + EvolutionChamber.JARS + " " + EvolutionChamber.PATH + "/" +name+".java");
		}catch(Exception e){
			e.printStackTrace();
		}
		return (name+".class");
	}
	
	public static double averageFitness(MetaRobot[] metarobots){
		double avg = 0;
		for(int i =0; i< metarobots.length;i++){
			avg += metarobots[i]._fitness;
		}
		avg /= metarobots.length;
		return avg;
	}
	
	public static MetaRobot findWorst(MetaRobot[] metaRobots) {
		double min = Double.MAX_VALUE;
		int robotMinFitness = -1;
	    for(int j=0; j<metaRobots.length; j++) {
	        if(metaRobots[j]._fitness < min ){
	            min = metaRobots[j]._fitness;
	        	robotMinFitness = j;
	        }
	    }
		return metaRobots[robotMinFitness];
	}
}
